package csvm_v1;

import csvm_v1.coins.Dime;
import csvm_v1.coins.Nickel;
import csvm_v1.coins.Quarter;
import csvm_v1.exceptions.WrongCommandArgumentException;
import csvm_v1.exceptions.WrongCommandFormatException;
import csvm_v1.products.CocaCola;
import csvm_v1.products.CustomProduct;
import csvm_v1.products.Fanta;
import csvm_v1.products.Product;
import csvm_v1.products.Sprite;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import utils.PriceUtility;
import utils.Triplet;

/**
 * CSVM_V1 adalah kelas bagi vending machine. Kelas CSVM_V1 menyediakan method
 * yang menggantikan fungsi dan prosedur bagi program yang berorientasi
 * prosedural. Method yang disediakan statik sehingga pemanggilan method hanya
 * seperti layaknya fungsi atau prosedur biasa
 *
 * @author Muqtafi Akhmad, Muhammad Anis
 */
public class CSVM_V1 {

    /**
     * katalog produk, memetakan indeks seleksi dengan produk
     */
    private static HashMap<Integer, Product> catalog;
    /**
     * jumlah koin dime input user
     */
    private static int user_input_dime_count = 0;
    /**
     * jumlah koin nickel input user
     */
    private static int user_input_nickel_count = 0;
    /**
     * jumlah koin quarter input user
     */
    private static int user_input_quarter_count = 0;
    /**
     * jumlah koin dime dalam vending machine
     */
    private static int user_change_dime_count = 0;
    /**
     * jumlah koin nickel dalam vending machine
     */
    private static int user_change_nickel_count = 0;
    /**
     * jumlah koin quarter dalam vending machine
     */
    private static int user_change_quarter_count = 0;
    /**
     * pemetaan produk dengan jumlah produk
     */
    private static HashMap<Product, Integer> productStock;
    /**
     * input command dari console
     */
    private static Scanner in = new Scanner(System.in);
    /**
     * command yang terbaca
     */
    private static String command;
    /**
     * nama file dump yang akan dibaca dan ditulisi dengan data dari vending
     * machine
     */
    private static final String VM_DUMP = "vm_dump.txt";

    /**
     * enumerasi status dari vending machine init : inisialisasi dari transaksi,
     * input : mengisikan koin, select : memilih produk, end : akhir dari
     * transaksi
     */
    private static enum vmStatus {

        init, input, select, end
    };
    /**
     * status dari vending machine
     */
    private static vmStatus status;

    /**
     * menginisialisasi vending machine, membaca data dari dump file
     *
     * @throws WrongCommandFormatException
     */
    private static void initVM() throws WrongCommandFormatException {
        try {
            Scanner fileReader = new Scanner(new File(VM_DUMP));
            catalog = new HashMap<>();
            productStock = new HashMap<>();
            // init catalog dan product stock, baca dari file
            // membaca header produk
            command = fileReader.nextLine();
            if (!command.equals("#products")) {
                throw (new WrongCommandFormatException("Tidak ditemukan header data produk"));
            }
            // membaca jumlah product dari file
            int i, jumlahProduct = Integer.parseInt(fileReader.nextLine());
            for (i = 1; i <= jumlahProduct; i++) {
                // membaca product satu per satu
                command = fileReader.nextLine();
                // format line untuk fixed product : <nama : {CocaCola, Fanta, Sprite}> <jumlah>
                // format line untuk custom product : <nama> <select index> <harga> <jumlah>
                String commandArgs[] = command.split(" ");
                switch (commandArgs[0]) {
                    case "CocaCola": {
                        CocaCola cola = new CocaCola();
                        catalog.put(cola.getSelectIndex(), cola);
                        productStock.put(cola, Integer.parseInt(commandArgs[1]));
                        break;
                    }
                    case "Fanta": {
                        Fanta fanta = new Fanta();
                        catalog.put(fanta.getSelectIndex(), fanta);
                        productStock.put(fanta, Integer.parseInt(commandArgs[1]));
                        break;
                    }
                    case "Sprite": {
                        Sprite sprite = new Sprite();
                        catalog.put(sprite.getSelectIndex(), sprite);
                        productStock.put(sprite, Integer.parseInt(commandArgs[1]));
                        break;
                    }
                    default: {
                        // custom product
                        CustomProduct produk = new CustomProduct(Integer.parseInt(commandArgs[2]), Integer.parseInt(commandArgs[1]), commandArgs[0]);
                        catalog.put(produk.getSelectIndex(), produk);
                        productStock.put(produk, Integer.parseInt(commandArgs[3]));
                        break;
                    }
                }
            }
            // membaca koin 
            // membaca header koin
            command = fileReader.nextLine();
            if (!command.equals("#coins")) {
                throw (new WrongCommandFormatException("Tidak ditemukan header data koin"));
            }
            int jumlahCoin = 3;
            for (i = 1; i <= jumlahCoin; i++) {
                command = fileReader.nextLine();
                // format line untuk coin : <nama> <jumlah>
                String commandArgs[] = command.split(" ");
                switch (commandArgs[0]) {
                    case "Dime": {
                        user_change_dime_count = Integer.parseInt(commandArgs[1]);
                        user_input_dime_count = 0;
                        break;
                    }
                    case "Nickel": {
                        user_change_nickel_count = Integer.parseInt(commandArgs[1]);
                        user_input_nickel_count = 0;
                        break;
                    }
                    case "Quarter": {
                        user_change_quarter_count = Integer.parseInt(commandArgs[1]);
                        user_input_quarter_count = 0;
                        break;
                    }
                    default: {
                        // do nothing
                        break;
                    }
                }
            }
        } catch (FileNotFoundException ex) {
            Logger.getLogger(CSVM_V1.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * menutup vending machine, menuliskan data ke dump file
     */
    private static void closeVM() {
        FileWriter fw = null;
        try {
            File file = new File(VM_DUMP);
            fw = new FileWriter(file.getAbsoluteFile());
            try (BufferedWriter bw = new BufferedWriter(fw)) {
                bw.write("#products\n");
                bw.write(catalog.size() + "\n");
                // menuliskan data produk
                Iterator it = catalog.entrySet().iterator();
                while (it.hasNext()) {
                    Map.Entry pairs = (Map.Entry) it.next();
                    Product produk = (Product) pairs.getValue();
                    Integer jumlah = (Integer) productStock.get(produk);
                    // format line untuk fixed product : <nama : {CocaCola, Fanta, Sprite}> <jumlah>
                    if (produk instanceof CocaCola) {
                        bw.write("CocaCola " + jumlah + "\n");
                    } else if (produk instanceof Fanta) {
                        bw.write("Fanta " + jumlah + "\n");
                    } else if (produk instanceof Sprite) {
                        bw.write("Sprite " + jumlah + "\n");
                    } else {
                        // format line untuk custom product : <nama> <select index> <harga> <jumlah>
                        bw.write(produk.getProductName() + " " + produk.getSelectIndex() + " " + produk.getPrice() + " " + jumlah + "\n");
                    }
                }
                // menuliskan header coin
                bw.write("#coins\n");
                bw.write("Dime " + user_change_dime_count + "\n");
                bw.write("Nickel " + user_change_nickel_count + "\n");
                bw.write("Quarter " + user_change_quarter_count + "\n");
            }
        } catch (IOException ex) {
            Logger.getLogger(CSVM_V1.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                fw.close();
            } catch (IOException ex) {
                Logger.getLogger(CSVM_V1.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /**
     * mempersiapkan sebuah transaksi
     */
    private static void initTransaction() {
        user_input_dime_count = 0;
        user_input_nickel_count = 0;
        user_input_quarter_count = 0;
        status = vmStatus.init;
    }

    /**
     * melakukan transaksi pada sejumlah produk
     *
     * @param produk produk yang dibeli
     * @param jumlah jumlah produk
     */
    private static void doTransaction(Product produk, Integer jumlah) {
        int totalMoney = user_input_dime_count * PriceUtility.DIME_VALUE
                + user_input_nickel_count * PriceUtility.NICKEL_VALUE
                + user_input_quarter_count * PriceUtility.QUARTER_VALUE;
        int lastProductCount = productStock.get(produk);
        if (jumlah == 0 || lastProductCount == 0) {
            System.out.println("Return customer money: ");
            System.out.println("Total: " + totalMoney);
            giveChange(totalMoney);

        } else {
            totalMoney -= produk.getPrice() * jumlah;
            if (giveChange(totalMoney)) {
                System.out.println("You get product: " + produk.getProductName() + " with count: " + jumlah);
                productStock.put(produk, lastProductCount - jumlah);
            } else {
                System.out.println("Sorry, insuficient changes for you");
                totalMoney = user_input_dime_count * PriceUtility.DIME_VALUE
                        + user_input_nickel_count * PriceUtility.NICKEL_VALUE
                        + user_input_quarter_count * PriceUtility.QUARTER_VALUE;
                giveChange(totalMoney);
            }
        }
        user_input_dime_count = 0;
        user_input_nickel_count = 0;
        user_input_quarter_count = 0;
    }

    /**
     * menambahkan sebuah koin ke vending machine
     *
     * @param size ukuran koin
     * @param weight berat koin
     * @param thickness ketebalan koin
     * @param isSerratedEdge keterangan bergerigi koin
     */
    private static void inputCoin(float size, float weight, float thickness, boolean isSerratedEdge) {
        if (Dime.isDime(size, weight, thickness, isSerratedEdge)) {
            user_input_dime_count++;
            user_change_dime_count++;
        } else if (Nickel.isNickel(size, weight, thickness, isSerratedEdge)) {
            user_input_nickel_count++;
            user_change_nickel_count++;
        } else if (Quarter.isQuarter(size, weight, thickness, isSerratedEdge)) {
            user_input_quarter_count++;
            user_change_quarter_count++;
        } else {
            System.out.println("slug terdeteksi, input ditolak");
        }
    }

    /**
     * mengembalikan nilai dari koin yang diinput
     *
     * @param dimeCount jumlah dime
     * @param nickleCount jumlah nickel
     * @param quarterCount jumlah quarter
     * @return nilai total koin
     */
    public static Integer getUserCoinValue(int dimeCount, int nickleCount, int quarterCount) {
        return PriceUtility.CoinToPrice(nickleCount, dimeCount, quarterCount);
    }

    /**
     * membeli sebuah produk
     *
     * @throws WrongCommandArgumentException
     */
    private static void buyProduct() throws WrongCommandArgumentException {
        initTransaction();
        status = vmStatus.input;
        while (status != vmStatus.end) {
            System.out.print("\n>>");
            switch (status) {
                case input: {
                    boolean edges = true;
                    System.out.println("Silakan masukkan koin ke vending machine format : size,weight,thickness,isSerratedEdge");
                    command = in.nextLine();
                    // split, input coin
                    String inputArgs[] = command.split(",");
                    if (inputArgs.length != 4) {
                        throw (new WrongCommandArgumentException("Input koin salah"));
                    }
                    if (inputArgs[3].equals("false")) {
                        edges = false;
                    }
                    inputCoin(Float.parseFloat(inputArgs[0]), Float.parseFloat(inputArgs[1]), Float.parseFloat(inputArgs[2]), edges);
                    System.out.println("Pilihan : \n1. input koin lagi\n2. pilih produk");
                    command = in.nextLine();
                    int pilihan = Integer.parseInt(command);
                    if (pilihan == 1) {
                        // do nothing
                    } else if (pilihan == 2) {
                        // ubah status menjadi select
                        status = vmStatus.select;
                    } else {
                        System.out.println("Pilihan tidak tersedia : " + pilihan);
                    }
                    break;
                }
                case select: {
                    System.out.println("Produk tersedia : ");
                    // print semua kode produk
                    Iterator it = catalog.entrySet().iterator();
                    while (it.hasNext()) {
                        Map.Entry pairs = (Map.Entry) it.next();
                        System.out.println(pairs.getKey() + "." + pairs.getValue());
                    }
                    System.out.println("Silakan masukkan kode pilihan produk : ");
                    command = in.nextLine();
                    Integer pilihan = Integer.parseInt(command);
                    if (catalog.containsKey(pilihan)) {
                        Product produk = (Product) catalog.get(pilihan);
                        System.out.println("Silakan masukkan jumlah produk : ");
                        command = in.nextLine();
                        Integer jumlah = Integer.parseInt(command);
                        Integer totalBayar = jumlah * produk.getPrice();
                        // cek apakah uang dalam box sudah memenuhi total bayar, jika tidak tanyakan apakah akan cancel pembelian?
                        if (totalBayar <= getUserCoinValue(user_input_dime_count, user_input_nickel_count, user_input_quarter_count)) {
                            doTransaction(produk, jumlah);
                        } else {
                            System.out.println("Uang yang anda masukkan kurang, Apakah Anda akan menambahkan koin? y / n");
                            command = in.nextLine();
                            if (command.equals("y")) {
                                status = vmStatus.input;
                            } else {
                                status = vmStatus.end;
                            }
                        }
                    } else {
                        System.out.println("Produk dengan kode " + pilihan + " tidak tersedia");
                    }
                    break;
                }
                default: {
                    break;
                }
            }
        }
    }

    /**
     * mengembalikan kembalian input koin dari pengguna
     *
     * @param price total harga yang dikembalikan
     * @return triplet jumlah koin quarter, dime, dan nickel
     */
    public static boolean giveChange(int price) {
        Triplet<Integer, Integer, Integer> changes = PriceUtility.PriceToCoin(user_change_dime_count, user_change_nickel_count, user_change_quarter_count, price);
        if (changes != null) {
            System.out.println("Changes QuarterCount : " + changes.getFirst());
            user_change_quarter_count -= changes.getFirst();
            System.out.println("Changes DimeCount : " + changes.getSecond());
            user_change_dime_count -= changes.getSecond();
            System.out.println("Changes NickelCount : " + changes.getThird());
            user_change_nickel_count -= changes.getThird();
            return true;
        }
        return false;

    }

    /**
     * algoritma program utama
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        try {
            initVM();
            System.out.println("Masukkan 'buy' untuk membeli product dan 'exit' untuk keluar program");
            command = in.nextLine();
            while (!command.equals("exit")) {
                if (command.equals("buy")) {
                    try {
                        buyProduct();
                    } catch (WrongCommandArgumentException ex) {
                        Logger.getLogger(CSVM_V1.class.getName()).log(Level.SEVERE, ex.getMessage());
                    } finally {
                        //jika terjadi kondisi dimana terjadi exception namun user telah berada pada state memilih produk
                        //pastikan terjadi transaksi apapun yang terjadi
                        if (user_input_dime_count != 0 || user_input_nickel_count != 0 || user_input_quarter_count != 0) {
                            doTransaction(null, 0);
                        }
                    }
                } else {
                    System.out.println("unknown command : " + command);
                }
                System.out.println("Masukkan 'buy' untuk membeli product dan 'exit' untuk keluar program");
                command = in.nextLine();
            }
            closeVM();
        } catch (WrongCommandFormatException ex) {
            Logger.getLogger(CSVM_V1.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
